package auth

import (
	"context"
	"crypto/rand"
	"encoding/hex"
	"errors"
	"fmt"
	"time"

	"iot-cloud-platform/internal/config"
	"iot-cloud-platform/internal/database"
	"iot-cloud-platform/internal/models"

	"github.com/golang-jwt/jwt/v5"
	"github.com/google/uuid"
	"golang.org/x/crypto/bcrypt"
	"gorm.io/gorm"
)

var (
	ErrInvalidCredentials      = errors.New("invalid credentials")
	ErrUserNotFound            = errors.New("user not found")
	ErrInvalidToken            = errors.New("invalid token")
	ErrTokenExpired            = errors.New("token expired")
	ErrInsufficientPermissions = errors.New("insufficient permissions")
)

// CustomClaims represents JWT claims for the IoT platform
type CustomClaims struct {
	UserID      uuid.UUID `json:"user_id"`
	Username    string    `json:"username"`
	Email       string    `json:"email"`
	Roles       []string  `json:"roles"`
	Permissions []string  `json:"permissions"`
	SessionID   uuid.UUID `json:"session_id"`
	jwt.RegisteredClaims
}

// AuthService handles authentication and authorization
type AuthService struct {
	db        *gorm.DB
	redis     *database.RedisManager
	config    *config.Config
	jwtSecret []byte
}

// NewAuthService creates a new authentication service
func NewAuthService(db *gorm.DB, redis *database.RedisManager, cfg *config.Config) *AuthService {
	return &AuthService{
		db:        db,
		redis:     redis,
		config:    cfg,
		jwtSecret: []byte(cfg.JWT.Secret),
	}
}

// LoginRequest represents a login request
type LoginRequest struct {
	Username string `json:"username" binding:"required"`
	Password string `json:"password" binding:"required"`
}

// LoginResponse represents a login response
type LoginResponse struct {
	AccessToken  string      `json:"access_token"`
	TokenType    string      `json:"token_type"`
	ExpiresIn    int64       `json:"expires_in"`
	RefreshToken string      `json:"refresh_token"`
	User         models.User `json:"user"`
}

// RegisterRequest represents a user registration request
type RegisterRequest struct {
	Username  string `json:"username" binding:"required,min=3,max=50"`
	Email     string `json:"email" binding:"required,email"`
	Password  string `json:"password" binding:"required,min=8"`
	FirstName string `json:"first_name"`
	LastName  string `json:"last_name"`
}

// Login authenticates a user and returns JWT tokens
func (a *AuthService) Login(ctx context.Context, req LoginRequest) (*LoginResponse, error) {
	// Find user by username or email
	var user models.User
	if err := a.db.Preload("Roles.Permissions").Where("username = ? OR email = ?", req.Username, req.Username).First(&user).Error; err != nil {
		if err == gorm.ErrRecordNotFound {
			return nil, ErrInvalidCredentials
		}
		return nil, fmt.Errorf("failed to find user: %w", err)
	}

	// Check if user is active
	if user.Status != models.UserStatusActive {
		return nil, ErrInvalidCredentials
	}

	// Verify password
	if err := bcrypt.CompareHashAndPassword([]byte(user.Password), []byte(req.Password)); err != nil {
		return nil, ErrInvalidCredentials
	}

	// Create user session
	session := models.UserSession{
		UserID:    user.ID,
		ExpiresAt: time.Now().Add(a.config.JWT.Expiry),
	}
	if err := a.db.Create(&session).Error; err != nil {
		return nil, fmt.Errorf("failed to create session: %w", err)
	}

	// Generate tokens
	accessToken, err := a.generateAccessToken(user, session.ID)
	if err != nil {
		return nil, fmt.Errorf("failed to generate access token: %w", err)
	}

	refreshToken, err := a.generateRefreshToken()
	if err != nil {
		return nil, fmt.Errorf("failed to generate refresh token: %w", err)
	}

	// Store refresh token in Redis
	refreshKey := fmt.Sprintf("refresh_token:%s", refreshToken)
	if err := a.redis.SetUserSession(ctx, refreshKey, session.ID.String(), int(a.config.JWT.Expiry.Seconds())); err != nil {
		return nil, fmt.Errorf("failed to store refresh token: %w", err)
	}

	// Update session with token
	session.Token = accessToken
	a.db.Save(&session)

	// Clean user password from response
	user.Password = ""

	return &LoginResponse{
		AccessToken:  accessToken,
		TokenType:    "Bearer",
		ExpiresIn:    int64(a.config.JWT.Expiry.Seconds()),
		RefreshToken: refreshToken,
		User:         user,
	}, nil
}

// Register creates a new user account
func (a *AuthService) Register(ctx context.Context, req RegisterRequest) (*models.User, error) {
	// Check if username or email already exists
	var existingUser models.User
	if err := a.db.Where("username = ? OR email = ?", req.Username, req.Email).First(&existingUser).Error; err == nil {
		return nil, fmt.Errorf("username or email already exists")
	}

	// Hash password
	hashedPassword, err := bcrypt.GenerateFromPassword([]byte(req.Password), bcrypt.DefaultCost)
	if err != nil {
		return nil, fmt.Errorf("failed to hash password: %w", err)
	}

	// Create user
	user := models.User{
		Username:  req.Username,
		Email:     req.Email,
		Password:  string(hashedPassword),
		FirstName: req.FirstName,
		LastName:  req.LastName,
		Status:    models.UserStatusActive,
	}

	if err := a.db.Create(&user).Error; err != nil {
		return nil, fmt.Errorf("failed to create user: %w", err)
	}

	// Assign default role (viewer)
	var viewerRole models.Role
	if err := a.db.Where("name = ?", "viewer").First(&viewerRole).Error; err == nil {
		a.db.Model(&user).Association("Roles").Append(&viewerRole)
	}

	// Clean password from response
	user.Password = ""
	return &user, nil
}

// Logout invalidates user session and tokens
func (a *AuthService) Logout(ctx context.Context, sessionID uuid.UUID) error {
	// Delete session from database
	if err := a.db.Delete(&models.UserSession{}, "id = ?", sessionID).Error; err != nil {
		return fmt.Errorf("failed to delete session: %w", err)
	}

	// Optionally delete associated refresh tokens
	// In a production system, you'd want to maintain a blacklist of invalidated tokens

	return nil
}

// RefreshToken generates a new access token using refresh token
func (a *AuthService) RefreshToken(ctx context.Context, refreshToken string) (*LoginResponse, error) {
	// Get session ID from refresh token
	refreshKey := fmt.Sprintf("refresh_token:%s", refreshToken)
	sessionIDStr, err := a.redis.GetUserSession(ctx, refreshKey)
	if err != nil {
		return nil, ErrInvalidToken
	}

	sessionID, err := uuid.Parse(sessionIDStr)
	if err != nil {
		return nil, ErrInvalidToken
	}

	// Get session and user
	var session models.UserSession
	if err := a.db.Preload("User.Roles.Permissions").First(&session, "id = ?", sessionID).Error; err != nil {
		return nil, ErrInvalidToken
	}

	// Check if session is expired
	if time.Now().After(session.ExpiresAt) {
		a.redis.DeleteUserSession(ctx, refreshKey)
		return nil, ErrTokenExpired
	}

	// Generate new access token
	accessToken, err := a.generateAccessToken(session.User, session.ID)
	if err != nil {
		return nil, fmt.Errorf("failed to generate access token: %w", err)
	}

	// Update session
	session.Token = accessToken
	session.UpdatedAt = time.Now()
	a.db.Save(&session)

	// Clean user password
	session.User.Password = ""

	return &LoginResponse{
		AccessToken:  accessToken,
		TokenType:    "Bearer",
		ExpiresIn:    int64(a.config.JWT.Expiry.Seconds()),
		RefreshToken: refreshToken,
		User:         session.User,
	}, nil
}

// ValidateToken validates and parses JWT token
func (a *AuthService) ValidateToken(tokenString string) (*CustomClaims, error) {
	token, err := jwt.ParseWithClaims(tokenString, &CustomClaims{}, func(token *jwt.Token) (interface{}, error) {
		if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok {
			return nil, fmt.Errorf("unexpected signing method: %v", token.Header["alg"])
		}
		return a.jwtSecret, nil
	})

	if err != nil {
		return nil, ErrInvalidToken
	}

	if claims, ok := token.Claims.(*CustomClaims); ok && token.Valid {
		// Check if token is expired
		if time.Now().After(claims.ExpiresAt.Time) {
			return nil, ErrTokenExpired
		}
		return claims, nil
	}

	return nil, ErrInvalidToken
}

// CheckPermission checks if user has required permission
func (a *AuthService) CheckPermission(userID uuid.UUID, resource, action string) (bool, error) {
	var user models.User
	if err := a.db.Preload("Roles.Permissions").First(&user, "id = ?", userID).Error; err != nil {
		return false, ErrUserNotFound
	}

	// Check if user has admin role (full access)
	for _, role := range user.Roles {
		if role.Name == "admin" {
			return true, nil
		}

		// Check specific permissions
		for _, permission := range role.Permissions {
			if permission.Resource == resource && (permission.Action == action || permission.Action == "*") {
				return true, nil
			}
		}
	}

	return false, nil
}

// generateAccessToken generates a JWT access token
func (a *AuthService) generateAccessToken(user models.User, sessionID uuid.UUID) (string, error) {
	// Extract roles and permissions
	roles := make([]string, len(user.Roles))
	permissionSet := make(map[string]bool)

	for i, role := range user.Roles {
		roles[i] = role.Name
		for _, permission := range role.Permissions {
			permissionSet[permission.Name] = true
		}
	}

	permissions := make([]string, 0, len(permissionSet))
	for permission := range permissionSet {
		permissions = append(permissions, permission)
	}

	// Create claims
	claims := CustomClaims{
		UserID:      user.ID,
		Username:    user.Username,
		Email:       user.Email,
		Roles:       roles,
		Permissions: permissions,
		SessionID:   sessionID,
		RegisteredClaims: jwt.RegisteredClaims{
			ExpiresAt: jwt.NewNumericDate(time.Now().Add(a.config.JWT.Expiry)),
			IssuedAt:  jwt.NewNumericDate(time.Now()),
			NotBefore: jwt.NewNumericDate(time.Now()),
			Issuer:    "iot-cloud-platform",
			Subject:   user.ID.String(),
		},
	}

	// Create token
	token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
	return token.SignedString(a.jwtSecret)
}

// generateRefreshToken generates a secure random refresh token
func (a *AuthService) generateRefreshToken() (string, error) {
	bytes := make([]byte, 32)
	if _, err := rand.Read(bytes); err != nil {
		return "", err
	}
	return hex.EncodeToString(bytes), nil
}

// GetUserByID retrieves user by ID
func (a *AuthService) GetUserByID(ctx context.Context, userID uuid.UUID) (*models.User, error) {
	var user models.User
	if err := a.db.Preload("Roles").First(&user, "id = ?", userID).Error; err != nil {
		if err == gorm.ErrRecordNotFound {
			return nil, ErrUserNotFound
		}
		return nil, fmt.Errorf("failed to get user: %w", err)
	}

	user.Password = ""
	return &user, nil
}

// UpdateUserProfile updates user profile information
func (a *AuthService) UpdateUserProfile(ctx context.Context, userID uuid.UUID, updates map[string]interface{}) (*models.User, error) {
	var user models.User
	if err := a.db.First(&user, "id = ?", userID).Error; err != nil {
		return nil, ErrUserNotFound
	}

	// Filter allowed fields
	allowedFields := map[string]bool{
		"first_name": true,
		"last_name":  true,
		"avatar":     true,
	}

	filteredUpdates := make(map[string]interface{})
	for key, value := range updates {
		if allowedFields[key] {
			filteredUpdates[key] = value
		}
	}

	if err := a.db.Model(&user).Updates(filteredUpdates).Error; err != nil {
		return nil, fmt.Errorf("failed to update user: %w", err)
	}

	user.Password = ""
	return &user, nil
}

// ChangePassword changes user password
func (a *AuthService) ChangePassword(ctx context.Context, userID uuid.UUID, oldPassword, newPassword string) error {
	var user models.User
	if err := a.db.First(&user, "id = ?", userID).Error; err != nil {
		return ErrUserNotFound
	}

	// Verify old password
	if err := bcrypt.CompareHashAndPassword([]byte(user.Password), []byte(oldPassword)); err != nil {
		return ErrInvalidCredentials
	}

	// Hash new password
	hashedPassword, err := bcrypt.GenerateFromPassword([]byte(newPassword), bcrypt.DefaultCost)
	if err != nil {
		return fmt.Errorf("failed to hash password: %w", err)
	}

	// Update password
	if err := a.db.Model(&user).Update("password", string(hashedPassword)).Error; err != nil {
		return fmt.Errorf("failed to update password: %w", err)
	}

	return nil
}
